Skip to content
This repository was archived by the owner on Dec 18, 2018. It is now read-only.

Handle response content length mismatches (#175)#1155

Merged
cesarblum merged 1 commit into
devfrom
cesarbs/track-response-content-length
Oct 11, 2016
Merged

Handle response content length mismatches (#175)#1155
cesarblum merged 1 commit into
devfrom
cesarbs/track-response-content-length

Conversation

@cesarblum

@cesarblum cesarblum commented Oct 7, 2016

Copy link
Copy Markdown
Contributor

#175

  • TODO: InvalidOperationException might not be the right exception type for this
  • TODO: is logging this as application errors OK or should we add new event IDs for response length mismatches?

@halter73 @Tratcher @mikeharder @davidfowl

@cesarblum
cesarblum force-pushed the cesarbs/track-response-content-length branch from 9c4f978 to 83a49d0 Compare October 7, 2016 17:25
var responseHeaders = FrameResponseHeaders;
if (responseHeaders != null && responseHeaders.HasContentLength)
{
_responseContentLength = int.Parse(responseHeaders.HeaderContentLength.ToString());

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TryParse, culture, format

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll leave it as Parse since that will throw if the app does something like httpContext.Response.Headers["Content-Length"] = "foo";

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In that case you should throw from the header setter ideally.

I think we should make sure we have the same logic we use in HttpAbstractions. We should probably look into sharing this with HttpAbsractions somehow.

It's also a good opportunity to make sure we use the same logic for parsing the request header content-length. Right now, it seems like we're probably more lax than we should be.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh. And I see you changed this to a long 👍 Were there not any tests failing when this was int.Parse?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No test failures :( It was bound to blow up somewhere.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wow. I thought our LargeDownload test wrote over 2GB like the LargeUpload test. It looks like the LargeDownload test only has a 1MB body.

We should change the LargeDownload test to be like the LargeUpload test where we check every byte value in a few megabyte scenario, but still ensure that sending over 2GB works.


protected void TryInitializeResponseContentLength()
{
if (!_responseContentLength.HasValue)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there's no content-length header this will re-run for every write. You need a flag to only run it once, or is HasContentLength really that fast?


if (_responseContentLength.HasValue && _responseBytesWritten < _responseContentLength.Value)
{
ThrowContentLengthMismatchException();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be a different message "Too few bytes written".

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why even throw at this point? Why not just directly invoke ReportApplicationError?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why have two different messages when we can have a single one that reports the error in the same way?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

because they are different conditions, and the message should be clear which is which.

protected void ThrowContentLengthMismatchException()
{
_keepAlive = false;
throw new InvalidOperationException($"Response content length mismatch: wrote {_responseBytesWritten} bytes to {_responseContentLength.Value}-byte response.");

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Too many bytes written

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, Stream allows InvalidOperationException.
https://msdn.microsoft.com/en-us/library/hh137799(v=vs.110).aspx

}

_responseBytesWritten += data.Count;
VerifyWrite();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this calling pattern is misleading, _responseBytesWritten gets incremented regardless of if you've written the bytes out. Invert it:

             VerifyWrite(data.Count);
             _responseBytesWritten += data.Count;

and/or move _responseBytesWritten += data.Count; inside of VerifyWrite and make it VerifyAndUpdateWrite

@cesarblum

Copy link
Copy Markdown
Contributor Author

@Tratcher I've addressed your comments.

var responseHeaders = FrameResponseHeaders;
if (responseHeaders != null && responseHeaders.HasContentLength)
{
_responseContentLength = long.Parse(responseHeaders.HeaderContentLength.ToString(), NumberStyles.None);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will result in a FormatException being thrown from Write. Use TryParse and ignore values you can't parse.
When determining if you should chunk, do you check for the presence of a content-length or if you understand it?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We check for its presence but we don't try to parse it.

I disagree with you on this point. We should throw if the Content-Length header is set to an invalid value. The whole point of this change is to track errors related to response Content-Length, this would be one of them. Without a valid value, we can't track it.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even if you want to throw, you can't throw a FormatException from Stream.Write. It needs to be wrapped in an InvalidOperationException

@cesarblum

Copy link
Copy Markdown
Contributor Author

Updated.

@cesarblum

Copy link
Copy Markdown
Contributor Author

Added validation to header setter. Not sure I covered all bases though. I'm thinking of adding a long _contentLength field to FrameHeaders, this would give fast access to that value instead of having to parse it from HeaderContentLength.

}
set
{
ValidateLongValue(value);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you cache the parsed value internally rather than re-parse it later?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, it's what I was referring to in my comment above.


public static void ValidateLongValue(StringValues value)
{
long.Parse(value, NumberStyles.AllowLeadingWhite | NumberStyles.AllowTrailingWhite, CultureInfo.InvariantCulture);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might make things a little easier to debug if this was wrapped in an InvalidOperationException stating the Content-Length header must be a positive decimal number or something like this.

@cesarblum

Copy link
Copy Markdown
Contributor Author

Updated. Caching Content-Length and wrapping FormatException in InvalidOperationException.

@cesarblum
cesarblum force-pushed the cesarbs/track-response-content-length branch from 1aa76e4 to a837285 Compare October 10, 2016 19:45

private long _bits = 0;
private HeaderReferences _headers;
private long? _contentLength;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this get cleared if I call Headers.Remove("Content-Length")?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. We should add a test case for this.

@cesarblum cesarblum Oct 10, 2016

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done a7e7044

@cesarblum
cesarblum force-pushed the cesarbs/track-response-content-length branch from a837285 to a7e7044 Compare October 10, 2016 21:57
@cesarblum

Copy link
Copy Markdown
Contributor Author

I'm applying the FrameHeaders change to FrameResponseHeaders only (1d3b71d)

"GET / HTTP/1.1",
"",
"");
await connection.ReceiveEnd(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would this result in a RST? or just a FIN?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FIN


using (var server = new TestServer(httpContext =>
{
httpContext.Response.ContentLength = 0;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if you added a version of this test that called httpContext.Respose.Body.Flush() here? Same result I hope.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same result. I've modified this test to test with and without flushing.

{{
if ({header.TestBit()})
{{
{{{If(header.Identifier == "ContentLength", () => $@"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missed loop.ClassName == "FrameResponseHeaders" && here.

{{
_bits = 0;
_headers = default(HeaderReferences);
_contentLength = null;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missed If here too.

protected Task TryProduceInvalidRequestResponse()
{
if (_requestRejected)
if (_requestRejected || _applicationException != null)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this change necessary?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Obviously the application could swallow the exception, so we shouldn't be relying on it. And this change has little to do with invalid requests, right? I guess we're now parsing the request content-length more strictly...

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the response hasn't already started by the time we detect a mismatch in the content length, we can send a 500 response. _applicationException is set when there's a mismatch, so we need this check to run the error response logic.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That should be handled by the normal call to ProduceEnd().

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed. My very first change (that I didn't even commit) was calling Abort() on a content length mismatch. This would prevent the call to ProduceEnd() in FrameOfT. But since I moved away from that decision, ProduceEnd() handles it as expected.

@halter73

Copy link
Copy Markdown
Member

I don't think we need a new event id for this. Treating it as a normal application error is fine with me.

@cesarblum

Copy link
Copy Markdown
Contributor Author

Updated.

@cesarblum
cesarblum force-pushed the cesarbs/track-response-content-length branch from 8e1eb31 to 6412753 Compare October 11, 2016 17:22
@halter73

Copy link
Copy Markdown
Member

:shipit:

_headers._raw{header.Identifier} = null;")}
}}
}}")}
{(loop.ClassName == "FrameResponseHeaders" ? "public long? HeaderContentLengthValue => _contentLength;" : "")}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I would probably put this in FrameResponseHeaders.cs

return $@"
using System;
using System.Collections.Generic;
using System.Globalization;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I don't think this is being used.

{{{Each(byLength, header => $@"
if (""{header.Name}"".Equals(key, StringComparison.OrdinalIgnoreCase))
{{
{{{If(loop.ClassName == "FrameResponseHeaders" && header.Identifier == "ContentLength", () => $@"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: $ is unnecessary.

}}
}}{
If(loop.ClassName == "FrameResponseHeaders" && header.Identifier == "ContentLength", () => $@"
_contentLength = ParseContentLength(value);")}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No $ needed

}}
set
{{
{{{If(loop.ClassName == "FrameResponseHeaders" && header.Identifier == "ContentLength", () => $@"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No $.

{{
if ({header.TestBit()})
{{
{{{If(loop.ClassName == "FrameResponseHeaders" && header.Identifier == "ContentLength", () => $@"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto

@cesarblum

Copy link
Copy Markdown
Contributor Author

@halter73 nits addressed :)

@cesarblum
cesarblum force-pushed the cesarbs/track-response-content-length branch from 2e6ffce to f8813a6 Compare October 11, 2016 20:25
@cesarblum
cesarblum merged commit f8813a6 into dev Oct 11, 2016
@cesarblum
cesarblum deleted the cesarbs/track-response-content-length branch October 11, 2016 20:32
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants